Cheatsheet [DS/A]

Built against your learning journal (last entry June 23) and training plan. Four interviews in two weeks means: patch the gap patterns below, drill the priority problems, and skip everything in the skip list. Every pattern panel now leads with when to use it (green) and the question tells (orange) that point to it β€” that's the recognition step interviewers actually grade. Pills mark who asks it: Google Plaid Both

Binary search β€” the gap template

When to use: the input is sorted (or monotonic β€” a yes/no that flips exactly once), or you're searching over an answer range where you can test "is X feasible?" in O(n). Turns O(n) scans into O(log n).
Tells: "sorted array" Β· "rotated sorted" Β· find the first/last/smallest/largest value that satisfies X Β· "minimize the maximum" / "maximum the minimum" Β· input size up to 1e9 so you can't iterate Β· answer is a threshold.
Two forms. Exact-match you know. The one to internalize is boundary search (find leftmost true) β€” it solves #153, #33, #981, and "first bad version" shapes. Invariant: lo is always in the unknown zone, hi the candidate answer.
# Exact match β€” lo <= hi, both move past mid
lo, hi = 0, len(arr) - 1
while lo <= hi:
    mid = (lo + hi) // 2
    if arr[mid] == target: return mid
    elif arr[mid] < target: lo = mid + 1
    else: hi = mid - 1

# Boundary β€” find leftmost index where cond(i) is True
# lo < hi, hi = mid (NOT mid-1): mid might BE the answer
lo, hi = 0, len(arr) - 1
while lo < hi:
    mid = (lo + hi) // 2
    if cond(mid): hi = mid      # keep mid as candidate
    else: lo = mid + 1
# lo == hi == answer

# 153: cond = arr[mid] <= arr[-1]  (mid is in the right/rotated half)
# 33:  find pivot with 153, then exact-match in the correct half
# 981: import bisect; i = bisect.bisect_right(times, t) - 1
  • Gotcha: hi = mid with lo <= hi loops forever. Boundary form pairs lo < hi with hi = mid; exact form pairs lo <= hi with mid Β± 1. Never mix.
  • Gotcha: rotated array β€” compare against arr[-1] (fixed anchor), not arr[lo] (moving target).
  • Know bisect_left vs bisect_right: left = first β‰₯ x, right = first > x.

Overlapping intervals β€” the gap template

When to use: you have a list of [start, end] ranges and need to merge, count, or detect overlaps β€” meetings, time windows, transaction periods, booked ranges. Almost always: sort by start first, then one linear pass.
Tells: "intervals" / "ranges" / "[start, end]" Β· merge overlapping Β· "can this person attend all meetings" Β· minimum rooms/resources Β· insert into a sorted set of ranges Β· "do any two overlap". If the problem gives you pairs and asks about overlap, it's this.
One template covers #56, #57, and Plaid's transaction-window merging: sort by start, then a single pass comparing each interval to the last merged one.
# 56. Merge Intervals β€” sort, then extend-or-append
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
    if start <= merged[-1][1]:            # overlaps last merged
        merged[-1][1] = max(merged[-1][1], end)
    else:
        merged.append([start, end])

# Overlap test between two intervals (memorize this form):
# a and b overlap  ⟺  a.start <= b.end and b.start <= a.end
Two templates β€” don't mix the sort key. Merge or count-overlap β†’ sort by START (above). Greedy scheduling β†’ sort by END (below): always keep the earliest-ending interval, which leaves the most room for the rest.
# 435. Non-overlapping Intervals β€” sort by END, greedy keep
intervals.sort(key=lambda x: x[1])
end = float("-inf"); keep = 0
for s, e in intervals:
    if s >= end:               # doesn't overlap the last kept
        keep += 1; end = e
return len(intervals) - keep      # removals; #452 arrows = keep itself

# 253. Meeting Rooms II β€” how many overlap at once (min rooms)
# min-heap of end times: push each end; if next start >= heap[0],
# pop first (reuse that room). Answer = max heap size seen.
  • Which sort? "merge them / how many at peak" β†’ sort by start. "max non-overlapping / fewest removals / fewest arrows" β†’ sort by end. Wrong key is the classic interval miss.
  • Gotcha: touching intervals β€” does [1,3] merge with [3,5]? Ask; it flips <= vs <. (In #435, s >= end treats touching as non-overlapping.)
  • Gotcha: max() on the end β€” [1,10] then [2,3] must not shrink the merged end to 3.
  • Plaid twist to expect: intervals arrive as transactions with timestamps + amounts; same merge, then aggregate per merged window.

Heap / Top-K β€” beyond Dijkstra

When to use: you need the k largest/smallest/most-frequent, or repeated access to the current min/max as data streams in. Heap gives O(log n) push/pop of the extreme without sorting everything.
Tells: "top K" / "k closest" / "k most frequent" Β· "largest/smallest" with a k Β· "median of a stream" Β· merge k sorted lists Β· schedule tasks by frequency/deadline Β· any "at each step take the biggest/smallest available".
You know heapq mechanics. The two shapes you haven't done: top-k with a size-k heap, and greedy scheduling.
# 215. Kth largest β€” MIN-heap of size k (not max-heap of size n)
# Root is always the kth largest seen so far.
import heapq
heap = []
for num in nums:
    heapq.heappush(heap, num)
    if len(heap) > k:
        heapq.heappop(heap)        # evict smallest
return heap[0]                      # O(n log k), not O(n log n)

# One-liners worth knowing:
heapq.nlargest(k, nums)             # also takes key=...
heapq.heapify(arr)                  # O(n), in place
  • Talking point: size-k min-heap gives O(n log k) β€” beats sorting when k β‰ͺ n. Interviewers ask for this comparison on #215.
  • #621 Task Scheduler: max-heap of counts + a cooldown queue of (ready_time, count). Greedy: always run the most frequent available task.

Design β€” LRU + TTL store

When to use: the problem asks you to build a class with specific method-level time guarantees β€” get/put in O(1), expiry, eviction. It's an API design + pick-the-right-data-structures exercise, not one algorithm.
Tells: "design a X" / "implement a class" Β· "each operation must be O(1)" Β· cache / eviction / capacity / LRU/LFU Β· "with a TTL" / "expires after" Β· get and set with a twist. Combo of hashmap (lookup) + ordering structure (recency/time) is the giveaway.
#146: dict gives O(1) lookup, recency order gives O(1) eviction. In Python, OrderedDict does both β€” lead with it, mention the dict + doubly-linked-list version as what it wraps.
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.cache = OrderedDict()

    def get(self, key):
        if key not in self.cache: return -1
        self.cache.move_to_end(key)      # mark most-recent
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.cap:
            self.cache.popitem(last=False)  # evict oldest

# Plaid variant β€” KV store with TTL (the #981 shape):
# store[key] = list of (timestamp, value); reads bisect on timestamp.
# Lazy expiry: check ts on read instead of background cleanup β€” say why
# (no timers needed, O(1) writes; tradeoff = stale entries hold memory).

Backtracking β€” one template, all three problems

When to use: you must enumerate all valid combinations/permutations/arrangements, or find one that satisfies constraints, by building a partial solution and undoing choices that dead-end. The search space is exponential and there's no clean formula.
Tells: "all possible" / "generate every" / "find all combinations/permutations/subsets" Β· partition / place N things without conflict (N-Queens, Sudoku) Β· word search on a grid Β· "return all paths" Β· small n (≀ ~12–15) because it's exponential.
Choose β†’ explore β†’ unchoose. The undo step is the whole pattern. #46/#78 on arrays, #79 on a grid (your grid-DFS skills transfer directly).
# 46. Permutations
def backtrack(path, remaining):
    if not remaining:
        result.append(path[:])          # COPY β€” path keeps mutating
        return
    for i in range(len(remaining)):
        path.append(remaining[i])                    # choose
        backtrack(path, remaining[:i] + remaining[i+1:])  # explore
        path.pop()                                   # unchoose

# 79. Word Search β€” grid DFS with undo:
# mark grid[r][c] = '#' before recursing, restore after. Same shape.
  • Gotcha: appending path instead of path[:] β€” every result ends up empty (all point to the same mutated list).
  • Gotcha: forgetting the restore in #79 β€” cells stay blocked for sibling paths, misses valid answers.

Trie β€” array children + pruning

When to use: repeated prefix lookups against a fixed dictionary of words β€” autocomplete, word search on a grid against a word list (#212), any "does this prefix/word exist" check done many times. Merges shared prefixes into one path instead of re-scanning per word.
Tells: "implement a dictionary/autocomplete" Β· multiple words searched against the same grid/text Β· "prefix" explicitly in the prompt.
For lowercase-only problems, a fixed 26-slot array beats a dict for children β€” array indexing, no hashing. Store the actual word (or None) at the terminal node instead of a plain is_end bool, so a match hands you the string directly.
# Prune a dead branch during backtracking β€” no parent pointer needed,
# the caller already has (parent) node + index in scope from the call site
child = node.children[char_index]
if child:
    ...
    backtrack(n_i, n_j, child)
    if child.word is None and all(c is None for c in child.children):
        node.children[char_index] = None   # nothing left through here, prune it
  • all(c is None for c in child.children) is the array-trie version of "does this node have no children left" β€” a dict's emptiness check (not child.children) isn't available on a fixed array, so scan the 26 slots instead. Cheap, constant work either way.
  • Prune at every level that recurses, including the outermost call (e.g. the root's direct children in the initial board scan) β€” pruning only inside the recursive helper leaves the top level uncleaned, so later starting cells keep re-entering already-exhausted branches.
  • #212 Word Search II specifically: unpruned trie+DFS is often correct but too slow on adversarial boards (dense repeated letters, many overlapping words) β€” this isn't an optional polish step for that problem.

Monotonic stack β€” next greater / smaller

When to use: for each element you need the nearest larger or smaller element to its left or right. A stack kept sorted (increasing or decreasing) answers all of these in one O(n) pass instead of O(nΒ²) nested loops.
Tells: "next greater/smaller element" Β· "how many days until a warmer temperature" Β· "largest rectangle" / "trapping rain water" Β· "stock span" Β· any "for each i, find the first j to the right that is bigger/smaller". Answer array same length as input.
You have #739 solid. The rule: pop while the stack top loses to the current element, then push. Whether you keep increasing or decreasing depends on next-greater vs next-smaller.
# 496/739. Next greater element to the right β€” decreasing stack of indices
res = [-1] * len(nums)
stack = []                          # holds indices, values decreasing
for i, x in enumerate(nums):
    while stack and nums[stack[-1]] < x:   # current x beats the top
        j = stack.pop()
        res[j] = x                  # x is j's next greater
    stack.append(i)
# 503 circular: iterate i in range(2n), use nums[i % n], only push i < n
  • Decide direction by the goal: next greater β†’ pop while top < x (stack stays decreasing). Next smaller β†’ pop while top > x.
  • Store indices, not values, when you need distance ("how many days"): i - j.
  • Circular array (#503): walk range(2*len) with i % n; only push real indices.

Prefix sum β€” range totals in O(1)

When to use: you make many range-sum queries, or you're counting subarrays whose sum equals / is divisible by k. Precompute a running total once, then any window is a subtraction. Prefix + hashmap turns "count subarrays summing to k" from O(nΒ²) to O(n).
Tells: "sum of subarray" / "range sum query" Β· "subarray summing to k" Β· "count subarrays where..." Β· "continuous subarray, sum divisible by k" Β· running balance / cumulative total Β· a fixed array with repeated sum(i..j) lookups. On a tree: "path sum equals target."
Two flavors: the prefix array for repeated range queries, and prefix + hashmap (the real interview move) for counting subarrays with a target sum β€” that's #560, and #437 is the same trick down a tree path.
# Range sum: prefix[i] = sum of first i elements; sum(l..r) = prefix[r+1]-prefix[l]
prefix = [0]
for x in nums: prefix.append(prefix[-1] + x)

# 560. Count subarrays summing to k β€” running sum + hashmap of seen sums
count = curr = 0
seen = {0: 1}                    # empty prefix seen once
for x in nums:
    curr += x
    count += seen.get(curr - k, 0)   # a prior prefix makes a window == k
    seen[curr] = seen.get(curr, 0) + 1
  • Seed the map with {0: 1} β€” otherwise you miss subarrays that start at index 0.
  • Count vs locate: hashmap of counts answers "how many"; hashmap of first index answers "longest such subarray" (#325).
  • Divisible-by-k variant: key on curr % k instead of curr.

Running total vs stored history β€” the O(1) space check

When to use: before reaching for a second array (prefix, suffix, or both), ask whether you need a fixed aggregate (sum, count, product) or whether you need to recall a specific past value. A fixed aggregate only needs one running number, computed once and carried forward. A specific past value needs actual storage.
Tells: "left/right sums are equal" Β· "everything except this index" Β· "running balance" Β· "min/max so far" β€” anywhere the answer at index i depends only on one number summarizing "everything not at i", not on which specific earlier value it was.
Contrast with prefix sum + hashmap above: #560 genuinely needs the hashmap, because you're asking "did this exact prefix value occur before", not just "what's the total" β€” one number can't answer that. #724 below only needs the total, because both sides are just sums.
# 724. Find Pivot Index β€” O(1) space, not O(n) prefix+suffix arrays
total = sum(nums)
left = 0
for i, x in enumerate(nums):
    right = total - left - x
    if left == right: return i
    left += x
return -1
  • Same trick, multiplicative: #238 Product of Array Except Self β€” running product leftβ†’right into the output array, then fold in a running product rightβ†’left. No separate left/right arrays.
  • Same trick, running extremum instead of running sum: #121 Best Time to Buy/Sell Stock (min-so-far) and #53 Maximum Subarray / Kadane's (max-ending-here).
  • The test: do you need one number (a total, min, or max), or do you need to remember which past values occurred? One number β†’ O(1) running variable. Which-value β†’ hashmap or array.

2D Grids β€” setup, iteration, neighbors

When to use: any matrix/board problem β€” flood fill, islands, word search, shortest path on a grid. Get instantiation and the neighbor-bounds check right once, reuse everywhere below (BFS/DFS/backtracking all sit on top of this).
Tells: "grid" / "board" / "matrix" input Β· "number of islands" Β· "flood fill" Β· rows/cols, 2D array of cells.
The #1 grid bug is aliasing: [[0]*cols]*rows makes every row the same list object, so mutating one row mutates all of them. Always build rows independently with a list comprehension.
# Instantiate β€” rows independent, no aliasing
rows, cols = 3, 4
grid = [[0] * cols for _ in range(rows)]
# bad_grid = [[0]*cols]*rows      # WRONG β€” all rows are one shared object

# Bounds-checked neighbor helper β€” reuse for BFS/DFS/backtracking
dirs_4 = [(-1,0), (1,0), (0,-1), (0,1)]

def get_neighbors(i, j, rows, cols):
    neighbors = []
    for dr, dc in dirs_4:
        ni, nj = i + dr, j + dc
        if 0 <= ni < rows and 0 <= nj < cols:
            neighbors.append((ni, nj))
    return neighbors
  • 8-direction (with diagonals): add (-1,-1),(-1,1),(1,-1),(1,1) to dirs_4.
  • Word Search-style problems: check the neighbor's value (e.g. board[ni][nj] == next_char) in the loop over get_neighbors's output β€” keep bounds-checking and value-checking as separate concerns, don't fold both into the helper.
  • rows, cols = len(grid), len(grid[0]) β€” compute once, don't call len() inside a hot loop.

BFS vs DFS β€” templates + when to use

When to use BFS: shortest path / fewest steps in an unweighted graph, or level-by-level processing. BFS reaches each node by the fewest edges, so first arrival = shortest. When to use DFS: explore whole paths β€” does a path exist, count components/islands, cycle detection, all-paths enumeration, tree traversals, flood fill, topological sort.
Tells β†’ BFS: "minimum moves/steps", "shortest path" (unweighted), "nearest", level order, something spreading (rotting oranges). Tells β†’ DFS: "all paths", "does a route exist", "count islands/regions", "cycle", any pre/in/post-order tree walk.
Both need a visited set on a graph or you loop forever. For BFS, mark visited when you enqueue, not when you dequeue, or the same node gets added many times.
# BFS β€” shortest steps, unweighted. Queue + level snapshot.
from collections import deque
q = deque([start]); seen = {start}; steps = 0
while q:
    for _ in range(len(q)):        # one whole level per steps++
        node = q.popleft()
        if node == target: return steps
        for nb in graph[node]:
            if nb not in seen:
                seen.add(nb); q.append(nb)   # mark on enqueue
    steps += 1

# DFS β€” explore fully. Recursion (implicit stack) + visited.
seen = set()
def dfs(node):
    seen.add(node)
    for nb in graph[node]:
        if nb not in seen:
            dfs(nb)
# Grid: neighbors = 4 dirs; guard 0<=r<R, 0<=c<C before recursing.
  • Shortest path is BFS only. DFS finds a path, not the shortest β€” the classic mix-up. Weighted edges β†’ neither, use Dijkstra.
  • Tree level-order (#102) is just the BFS template without a seen set (a tree can't revisit). The inner for loop is the level snapshot for #199 / zigzag.
  • Deep DFS can blow the recursion stack (~1000 in Python). If depth could be large, convert to an explicit stack loop.

Google β€” what the round looks like

  • Style: classic single problem + follow-ups that tighten constraints ("now do it in O(1) space", "now the input doesn't fit in memory"). The follow-up is the real evaluation.
  • Highest-frequency patterns for you: sliding window βœ“, trees/graphs βœ“, binary search βš‘, heap βš‘, LRU βš‘ β€” your gaps are exactly their favorites, hence the priority order.
  • State brute force first, then optimize β€” they score the journey.
  • Complexity precision matters: "O(n log k), and here's the log k" β€” your journal's Dijkstra-complexity habit is exactly the bar.
  • They rarely ask DP at L4/L5 phone screens, but #70/#198-level recurrence fluency is cheap insurance if a mock goes well early.

Python gotchas β€” quick traps to catch in any round

  • Presence check vs truthiness: when a value can legitimately be 0 or empty, test if x is not None, not if not x. 0, 0.0, "", [], False are all falsy β€” a real value of 0 makes not x wrongly read as "missing." (Bit me on #56 with a sentinel last_end == 0.)
  • Sort by a field: sorted(xs, key=lambda x: x[0]); tuple key for tiebreaks key=lambda x: (x[0], x[1]). sorted() returns new, .sort() mutates in place.

Priority problems β€” 2 weeks, ranked

OrderProblemsPatternWhy now
1#33, #153, #981 BothBinary searchBiggest gap Γ— highest Google frequency. #981 bridges into design.
2#56, #57 BothIntervalsPlaid's signature shape; Google staple. Two problems, one template.
3#146 LRU Cache BothDesignTop-5 Google frequency for years; Plaid asks the TTL-store cousin.
4#215, #621 GoogleHeapYou have the mechanics; these teach the top-k / greedy-schedule shapes.
5#79, #46 GoogleBacktracking#79 = grid DFS + undo; #46 = the canonical choose/explore/unchoose.
6Rate limiter Β· dedupe transactions Β· parse nested accounts PlaidPracticalNo LeetCode number β€” practice the shape: dict + timestamps + evolving requirements (see Plaid panel).
Cadence: 1 gap problem/day on weekdays β‰ˆ all of tiers 1–4 in week one, tiers 5–6 plus timed re-solves in week two. Log each in learning_journal.html as usual.
Coverage from learning_journal.html (through June 23) + interview_training_plan.html. Log new solves in the journal as you go β€” say the word and I'll add these to the plan schedule.